package com.ssdp.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ssdp.constant.DefaultUserConstant;
import com.ssdp.constant.RedisConstant;
import com.ssdp.constant.SMSConstant;
import com.ssdp.dto.LoginFormDTO;
import com.ssdp.dto.Result;
import com.ssdp.dto.UserDTO;
import com.ssdp.entity.User;
import com.ssdp.mapper.UserMapper;
import com.ssdp.service.IUserService;
import com.ssdp.utils.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpSession;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static com.ssdp.constant.RedisConstant.*;

/**
 * @author syo 2319615164@qq.com
 * @description 用户服务
 * @date 2024/3/14
 * @Copyright syo
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    private final UserMapper userMapper;

    private final StringRedisTemplate stringRedisTemplate;

    /**
     * 短信验证码的发送
     * @param phone 电话号码
     * @param session session
     * @return 返回通用结果
     */
    @Override
    public Result sendCode(String phone, HttpSession session) {
        // 1. 校验电话号码
        if(RegexUtils.isPhoneInvalid(phone)) {

        // 2. 验证失败返回
            return Result.fail("手机号格式错误！");
        }

        // 3. 验证成功

        // 4. 生成验证码
        String verifyCode = RandomUtil.randomNumbers(6);


        // 5. 将验证码放入session/redis
        // key : login:code:${phone}
        // value : ${verifyCode}
        // 如果放入session: session.setAttribute(phone, verifyCode);
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone,
                verifyCode, LOGIN_CODE_TTL, TimeUnit.MINUTES);

        // 6. 发送验证码(这儿应该调用工具类中的方法，这儿简化使用日志)
        SMSUtils.sendMessage(
                SMSConstant.SIGN_NAME,
                SMSConstant.TEMPLATE_CODE,
                phone,
                verifyCode);

        log.info("验证码为：{}", verifyCode);

        // 7. 返回成功
        return Result.ok();
    }

    /* 使用session登录的业务逻辑(废弃) */
    @Deprecated
    public Result login01(LoginFormDTO loginForm, HttpSession session) {
        // 1. 校验用户手机号
        String phone = loginForm.getPhone();

        if(RegexUtils.isPhoneInvalid(phone)) {
            // 2. 验证失败返回
            return Result.fail("手机号格式错误！");
        }

        // 6. 根据手机号查询用户
        QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
        userQueryWrapper.eq("phone", phone);
        User user = userMapper.selectOne(userQueryWrapper);

        // 7. 如果用户不存在则创建用户
        if(user == null) {
            user = createUserWithPhone(phone);
            userMapper.insert(user); // 新建的用户存入数据库
        }

        // 8. 用户存在则将用户放入session redis
        //session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
        String token = UUID.randomUUID().toString(true);
        String key = LOGIN_USER_KEY + token;
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);

        // 将对象转换为map，并且让不是字符串类型的转换为字符串类型
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor(
                        (fieldName, fieldValue) -> fieldValue.toString()
                ));

        stringRedisTemplate.opsForHash().putAll(key, userMap);
        stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);

        // 9. 返回
        return Result.ok(token);
    }

    /**
     * 用户登录
     * @param loginForm 登录表单
     * @param session session
     * @return 带有token的结果
     */
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        // 1. 校验用户手机号
        String phone = loginForm.getPhone();
        if(RegexUtils.isPhoneInvalid(phone)) {
        // 2. 验证失败返回
            return Result.fail("手机号格式错误！");
        }

        // 3. 校验验证码
        String code = loginForm.getCode();
        if(RegexUtils.isCodeInvalid(code)) {
        // 4. 验证失败
            return Result.fail("验证码格式错误！");
        }

        // 5. 比对验证码
        String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        if(cacheCode == null || cacheCode.isEmpty() || !cacheCode.equals(code)) {
            return Result.fail("验证码错误！");
        }

        // 6. 根据手机号查询用户
        QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
        userQueryWrapper.eq("phone", phone);
        User user = userMapper.selectOne(userQueryWrapper);

        // 7. 如果用户不存在则创建用户
        if(user == null) {
            user = createUserWithPhone(phone);
            userMapper.insert(user);
        }

        // 8. 用户存在则将用户放入session/redis
        // session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
        String token = UUID.randomUUID().toString(true);
        String key = LOGIN_USER_KEY + token;

        // 存入UserDTO对象到redis中，避免敏感信息泄露
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        userDTO.setToken(token);

        // 将对象转换为map，并且让不是字符串类型值的转换为字符串类型
        // 如果不转换将会报错，因为StringRedisTemplate要求键值对都为字符串
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                CopyOptions.create().setIgnoreNullValue(false).setFieldValueEditor(
                        (fieldName, fieldValue) -> fieldValue.toString()
                ));

        // 使用Hash结果存储
        stringRedisTemplate.opsForHash().putAll(key, userMap);
        stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);

        // 9. 删除验证码
        stringRedisTemplate.delete(LOGIN_CODE_KEY + phone);

        // 10. 返回token
        return Result.ok(token);
    }

    /**
     * 用户签到（使用BitMap数据类型）
     * @return 返回签到是否成功
     */
    @Override
    public Result sign() {
        // 1. 获取当前登录的用户
        Long id = UserHolder.getUser().getId();

        // 2. 获取日期
        LocalDateTime now = LocalDateTime.now();

        // 3. 拼接key（需要拼接某年某月）
        String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
        String key = USER_SIGN_KEY + id + keySuffix;

        // 4. 获取今天是本月的第几天
        int dayOfMonth = now.getDayOfMonth();
        int bitOffset = dayOfMonth - 1;

        // 5. 写入redis
        stringRedisTemplate.opsForValue().setBit(key, bitOffset, true);

        return Result.ok();
    }

    /**
     * 签到数目
     * @return 通用结果
     */
    @Override
    public Result signCount() {
        // 1. 获取当前登录的用户
        Long id = UserHolder.getUser().getId();

        // 2. 获取日期
        LocalDateTime now = LocalDateTime.now();

        // 3. 拼接key
        String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
        String key = USER_SIGN_KEY + id + keySuffix;

        // 4. 获取今天是本月的第几天
        int dayOfMonth = now.getDayOfMonth();

        // 5. 获取本月到今天的所有签到记录
        List<Long> result = stringRedisTemplate.opsForValue().bitField(key,
                BitFieldSubCommands
                        .create()
                        .get(BitFieldSubCommands
                                .BitFieldType.unsigned(dayOfMonth)).valueAt(0));
        if(result == null || result.isEmpty()) {
            return Result.ok();
        }

        Long num = result.get(0);

        if(num == null || num.equals(0L)) {
            return  Result.ok();
        }

        // 6. 循环遍历
        long realNum = num >>> 1;
        int count = 0;
        while(true) {
            if( (realNum & 1) == 0) {
                break;
            } else {
                count++;
            }
            realNum >>>= 1;
        }

        // 7. 让这个数字与1做与运算
        // 判断bit位是否为0
        // 不为0则已签到，计数器 + 1
        if((num & 1) == 1) {
            count++;
        }

        return Result.ok(count);
    }

    /**
     * 用户登出
     */
    @Override
    public void logout() {
        // 后端移出线程中的user即可，Redis中的token30分钟会自动失效
        // 前端需要移出本地的token
        String token = RedisConstant.LOGIN_USER_KEY + UserHolder.getUser().getToken();
        stringRedisTemplate.delete(token);
        UserHolder.removeUser();
    }

    /**
     * 创建新用户
     * @param phone 用户电话号
     * @return 用户对象
     */
    private User createUserWithPhone(String phone) {
        User user = new User();
        user.setPhone(phone);
        user.setNickName(DefaultUserConstant.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
        return user;
    }
}
